<?php

namespace pmvc\util;

use pmvc\log\Log;
use pmvc\ClassLoader;
use pmvc\PharClassLoader;
use pmvc\ArchiveClassLoader;
use pmvc\FileSystemClassLoader;
use pmvc\aop\ProxyUtil;

use ReflectionClass;
use DirectoryIterator;

/**
 * Utilities for class loading.
 *
 */
class ClassUtil {

	private static $CLASS_LOADERS = Array();

	/**
	 * Returns an array of the {@link ClassLoader}s that
	 * are registered.
	 * @return Array of {@link ClassLoader}
	 */
	public static function getClassLoaders() {
		$ret = Array();
		for ($i=count(self::$CLASS_LOADERS)-1; $i>=0; $i--) {
			$ret []= self::$CLASS_LOADERS[$i]['classLoader'];
		}
		return $ret;
	}

	/**
	 * Uses all of the registered class loaders to require
	 * the given file.
	 * @param $fileName the file to load
	 */
	public static function requireFile($fileName, $once=false) {
		for ($i=count(self::$CLASS_LOADERS)-1; $i>=0; $i--) {
			$classLoader = self::$CLASS_LOADERS[$i]['classLoader'];
			if ($classLoader->requireFile($fileName, $once)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns the path to the given file
	 * that must exist in the classpath.
	 * @param string $file the file to map
	 * @return the file name, or false if it wasn't found
	 */
	public static function getResourcePath($file) {
		for ($i=count(self::$CLASS_LOADERS)-1; $i>=0; $i--) {
			$resource = self::$CLASS_LOADERS[$i]['classLoader']->getResourcePath($file);
			if ($resource!==false) {
				return $resource;
			}
		}
		return false;
	}

	/**
	 * Returns all resource paths for the given
	 * path with optional recursiveness.
	 * @param $file the path
	 * @param $recursive whether or not to be recursive
	 */
	public static function getResourcePaths($file, $recursive=true) {
		$ret = Array();
		for ($i=count(self::$CLASS_LOADERS)-1; $i>=0; $i--) {
			$resources = self::$CLASS_LOADERS[$i]['classLoader']->getResourcePaths($file, $recursive);
			if ($resources!==false) {
				$ret = array_merge($ret, $resources);
			}
		}
		return $ret;
	}

	/**
	 * Returns a {@link pmvc\io\Stream} to the given file
	 * found in the classpath.  false
	 * is returned if the ClassLoader is unable
	 * to locate the given file.
	 * @param string $file the file
	 * @return the {@link pmvc\io\Stream}, or false if it wasn't found
	 */
	public static function getResourceAsStream($file) {
		for ($i=count(self::$CLASS_LOADERS)-1; $i>=0; $i--) {
			$stream = self::$CLASS_LOADERS[$i]['classLoader']->getResourceAsStream($fileName);
			if ($stream!==false) {
				return $stream;
			}
		}
		return false;
	}
	
	/**
	 * Adds a {@link ClassLoader} to the class loading
	 * sub system.
	 * @param pmvc\ClassLoader $classLoader
	 */
	public static function addClassLoader(ClassLoader $classLoader) {
		$loader = Array(
			'classLoader' => $classLoader,
			'func' => function($className)use($classLoader) {
				return $classLoader->loadClass($className);
			}
		);
		array_push(self::$CLASS_LOADERS, $loader);
		spl_autoload_register($loader['func'], false);
	}
	
	/**
	 * Removes a {@link ClassLoader} from the class loading
	 * sub system.
	 * @param pmvc\ClassLoader $classLoader
	 */
	public static function removeClassLoader(ClassLoader $classLoader) {
		$newLoaders = Array();
		for ($i=0; $i<count(self::$CLASS_LOADERS); $i++) {
			$loader = self::$CLASS_LOADERS[$i];
			if ($loader['classLoader']==$classLoader) {
				spl_autoload_unregister($loader['func']);
			} else {
				array_push($newLoaders, $loader);
			}
		}
		self::$CLASS_LOADERS = $newLoaders;
	}

	/**
	 * Creates an adds {@link ClassLoader}s for the given paths.
	 * If {@code $expandArchives} is true and {@code $expansionDirectory}
	 * is unset then archives are expanded in the same directory that
	 * the archive resides with a directory name the same as the archive
	 * and {@code -expanded} appended to the name.
	 * 
	 * @param array $paths the paths
	 * @param bool $expandArchives whether or not to expand archives
	 * @param string $expansionDirectory the directory to expand archives
	 */
	public static function addClassLoadersForPaths(
		Array $paths, $expandArchives=false, $expansionDirectory=null) {
		foreach (self::createClassLoadersForPaths(
			$paths, $expandArchives, $expansionDirectory) as $loader) {
			self::addClassLoader($loader);
		}
	}

	/**
	 * Creates an array of {@link ClassLoader}s for the given paths.
	 * If {@code $expandArchives} is true and {@code $expansionDirectory}
	 * is unset then archives are expanded in the same directory that
	 * the archive resides with a directory name the same as the archive
	 * and {@code -expanded} appended to the name.
	 * 
	 * @param array $paths the paths
	 * @param bool $expandArchives whether or not to expand archives
	 * @param string $expansionDirectory the directory to expand archives
	 * @return array an array of {@link ClassLoader}s
	 */
	public static function createClassLoadersForPaths(
		Array $paths, $expandArchives=false, $expansionDirectory=null) {
		$classLoaders = Array(); 
		$classLoader = NULL;
		
		foreach ($paths as $path) {

			// PharClassLoader
			if (StringUtil::startsWith($path, PharClassLoader::PHAR_PREFIX, true)
				|| (is_file($path) && (StringUtil::endsWith($path, 'phar', true)))) {
				if (!isset($classLoader)) {
					$classLoader = new PharClassLoader(Array($path));
				} else if (isset($classLoader) && !is_a($classLoader, 'pmvc\PharClassLoader')) {
					$classLoaders []= $classLoader;
					$classLoader = new PharClassLoader(Array($path));
				} else if (isset($classLoader) && is_a($classLoader, 'pmvc\PharClassLoader')) {
					$classLoader->addToClassPath($path);
				}

			// ArchiveClassLoader
			} else if (is_file($path) && (
					StringUtil::endsWith($path, 'tar.gz', true)
					|| StringUtil::endsWith($path, 'tgz', true)
					|| StringUtil::endsWith($path, 'tar', true)
					|| StringUtil::endsWith($path, 'zip', true)
					|| StringUtil::endsWith($path, 'rar', true)
					)) {
				if (!isset($classLoader)) {
					$classLoader = new ArchiveClassLoader(Array($path), $expandArchives, $expansionDirectory);
				} else if (isset($classLoader) && !is_a($classLoader, 'pmvc\ArchiveClassLoader')) {
					$classLoaders []= $classLoader;
					$classLoader = new ArchiveClassLoader(Array($path), $expandArchives, $expansionDirectory);
				} else if (isset($classLoader) && is_a($classLoader, 'pmvc\ArchiveClassLoader')) {
					$classLoader->addToClassPath($path);
				}

			// FileSystemClassLoader
			} else if (is_dir($path)) {
				if (!isset($classLoader)) {
					$classLoader = new FileSystemClassLoader(Array($path));
				} else if (isset($classLoader) && !is_a($classLoader, 'pmvc\FileSystemClassLoader')) {
					$classLoaders []= $classLoader;
					$classLoader = new FileSystemClassLoader(Array($path));
				} else if (isset($classLoader) && is_a($classLoader, 'pmvc\FileSystemClassLoader')) {
					$classLoader->addToClassPath($path);
				}
			}
		}

		// make sure we add the last ClassLoader
		if (isset($classLoader)) {
			$classLoaders[] = $classLoader;
		}

		// return it
		return $classLoaders;
	}

	/**
	 * Reads path elements from the given directories as
	 * classpath elements.
	 * @param string $directory
	 * @return array an array of path elements that can be
	 * 	used in a class path
	 */
	public static function readDirectoriesAsPathElements(Array $directories) {
		$classPath = Array();
		foreach ($directories as $directory) {
			if (file_exists($directory) && is_dir($directory)) {
				$itr = new DirectoryIterator($directory);
				foreach ($itr as $info) {
					if (!$info->isDot()) {
						$classPath []= $info->getPath().DIRECTORY_SEPARATOR.$info->getFilename();
					}
				}
			}
		}
		return $classPath;
	}

}

?>